package metadata

import (
	"encoding/json"
	"fmt"

	"github.com/romberli/das/config"
	"github.com/romberli/das/internal/dependency/metadata"
	"github.com/romberli/das/pkg/util/deployment"
	utilmeta "github.com/romberli/das/pkg/util/metadata"
	"github.com/romberli/go-util/common"
	"github.com/romberli/go-util/constant"
	"github.com/romberli/log"
	"github.com/spf13/viper"
)

const (
	tableTablesStruct = "Tables"

	tableTablesJSON = "tables"
)

var _ metadata.TableService = (*TableService)(nil)

// TableService implements dependency.TableService
type TableService struct {
	metadata.TableRepo
	Tables          []metadata.Table          `middleware:"tables" json:"tables"`
	TableStatistics []metadata.TableStatistic `middleware:"table_statistics" json:"table_statistics"`
	IndexStatistics []metadata.IndexStatistic `middleware:"index_statistics" json:"index_statistics"`
	CreateStatement string                    `middleware:"create_statement" json:"create_statement"`
}

// NewTableService returns metadata.TableService
func NewTableService(repo metadata.TableRepo) metadata.TableService {
	return newTableService(repo)
}

// NewTableServiceWithDefault returns metadata.TableService
func NewTableServiceWithDefault() metadata.TableService {
	return newTableService(NewTableRepoWithDefault())
}

// newTableService returns *TableService
func newTableService(repo metadata.TableRepo) *TableService {
	return &TableService{
		repo,
		[]metadata.Table{},
		[]metadata.TableStatistic{},
		[]metadata.IndexStatistic{},
		constant.EmptyString,
	}
}

// GetTables returns the tables list
func (ts *TableService) GetTables() []metadata.Table {
	return ts.Tables
}

// GetTableStatistics returns the table statistics list
func (ts *TableService) GetTableStatistics() []metadata.TableStatistic {
	return ts.TableStatistics
}

// GetIndexStatistics returns the index statistics list
func (ts *TableService) GetIndexStatistics() []metadata.IndexStatistic {
	return ts.IndexStatistics
}

// GetCreateStatement returns the create statement of table
func (ts *TableService) GetCreateStatement() string {
	return ts.CreateStatement
}

// GetByDBIDWithHTTP gets the tables by db id
func (ts *TableService) GetByDBIDWithHTTP(dbID int, loginName string) error {
	hostIP, portNum, dbName, err := ts.getHostInfoAndDBNameByDBIDWithHTTP(dbID)
	if err != nil {
		return err
	}

	return ts.getByHostInfoAndDBNameWithHTTP(hostIP, portNum, dbName, loginName)
}

// GetByHostInfoAndDBNameWithHTTP gets the tables by host info and db name with http
func (ts *TableService) GetByHostInfoAndDBNameWithHTTP(hostIP string, portNum int, dbName, loginName string) error {
	return ts.getByHostInfoAndDBNameWithHTTP(hostIP, portNum, dbName, loginName)
}

// GetStatisticsByDBIDAndTableNameWithHTTP gets the table statistics by db id and table name with http
func (ts *TableService) GetStatisticsByDBIDAndTableNameWithHTTP(dbID int, tableName string, loginName string) error {
	hostIP, portNum, dbName, err := ts.getHostInfoAndDBNameByDBIDWithHTTP(dbID)
	if err != nil {
		return err
	}

	return ts.getStatisticsByHostInfoAndDBNameAndTableNameWithHTTP(hostIP, portNum, dbName, tableName, loginName)
}

// GetStatisticsByHostInfoAndDBNameAndTableName gets the table statistics by host info and db name and table name with http
func (ts *TableService) GetStatisticsByHostInfoAndDBNameAndTableName(hostIP string, portNum int, dbName, tableName string, loginName string) error {
	return ts.getStatisticsByHostInfoAndDBNameAndTableNameWithHTTP(hostIP, portNum, dbName, tableName, loginName)
}

// AnalyzeTableByDBIDAndTableName analyzes the table by db id and table name with http
func (ts *TableService) AnalyzeTableByDBIDAndTableName(dbID int, tableName string, loginName string) error {
	hostIP, portNum, dbName, err := ts.getHostInfoAndDBNameByDBIDWithHTTP(dbID)
	if err != nil {
		return err
	}

	return ts.analyzeTableByHostInfoAndDBNameAndTableNameWithHTTP(hostIP, portNum, dbName, tableName, loginName)
}

// AnalyzeTableByHostInfoAndDBNameAndTableName analyzes the table by host info and db name and table name with http
func (ts *TableService) AnalyzeTableByHostInfoAndDBNameAndTableName(hostIP string, portNum int, dbName, tableName string, loginName string) error {
	return ts.analyzeTableByHostInfoAndDBNameAndTableNameWithHTTP(hostIP, portNum, dbName, tableName, loginName)
}

// Marshal marshals TableService.Tables to json bytes
func (ts *TableService) Marshal() ([]byte, error) {
	return ts.MarshalWithFields(tableTablesStruct)
}

// MarshalWithFields marshals only specified fields of the TableService to json bytes
func (ts *TableService) MarshalWithFields(fields ...string) ([]byte, error) {
	return common.MarshalStructWithFields(ts, fields...)
}

// getHostInfoAndDBNameByDBIDWithHTTP gets the host info and db name with http
func (ts *TableService) getHostInfoAndDBNameByDBIDWithHTTP(dbID int) (string, int, string, error) {
	ds := NewDBServiceWithDefault()

	err := ds.GetByID(dbID)
	if err != nil {
		return constant.EmptyString, constant.ZeroInt, constant.EmptyString, err
	}
	err = ds.GetMySQLClusterByID(dbID)
	if err != nil {
		return constant.EmptyString, constant.ZeroInt, constant.EmptyString, err
	}

	masterServers, err := ds.GetMySQLCluster().GetMasterServersWithHTTP()
	if err != nil {
		return constant.EmptyString, constant.ZeroInt, constant.EmptyString, err
	}

	return masterServers[constant.ZeroInt].GetHostIP(), masterServers[constant.ZeroInt].GetPortNum(), ds.GetDBs()[constant.ZeroInt].GetDBName(), nil
}

// getByHostInfoAndDBNameWithHTTP gets the tables by host info and db name with http
func (ts *TableService) getByHostInfoAndDBNameWithHTTP(hostIP string, portNum int, dbName, loginName string) error {
	if viper.GetString(config.RoleKey) == config.ServerRole {
		// get mysql server
		mysqlService := NewMySQLServerServiceWithDefault()
		err := mysqlService.GetByHostInfo(hostIP, portNum)
		if err != nil {
			return err
		}
		// check accessibility
		deploymentService := deployment.NewServiceWithDefault(mysqlService.GetMySQLServers()[constant.ZeroInt])
		if !deploymentService.CheckAccessibility() {
			// prepare data
			data := []byte(fmt.Sprintf(utilmeta.TableHostInfoAndDBNameHTTPBodyTemplate, hostIP, portNum, dbName, loginName))
			// send request
			respBody, err := deploymentService.PostDAS(utilmeta.TableGetByHostInfoAndDBNamePath, data)
			if err != nil {
				return err
			}
			// unmarshal
			var result map[string][]*TableInfo

			err = json.Unmarshal(respBody, &result)
			if err != nil {
				return err
			}
			// prepare result
			ts.Tables = nil
			for _, table := range result[tableTablesJSON] {
				ts.Tables = append(ts.Tables, table)
			}

			return nil
		}
	}

	return ts.GetByHostInfoAndDBNameLocal(hostIP, portNum, dbName)
}

// GetByHostInfoAndDBNameLocal gets the tables by host info and db name
func (ts *TableService) GetByHostInfoAndDBNameLocal(hostIP string, portNum int, dbName string) error {
	// mysql server is available or the role is agent
	err := ts.TableRepo.InitMySQLConn(hostIP, portNum, dbName)
	if err != nil {
		return err
	}
	defer func() {
		closeErr := ts.TableRepo.Close()
		if closeErr != nil {
			log.Errorf("metadata TableService.GetByHostInfoAndDBNameLocal(): close mysql connection failed. error:\n%+v", closeErr)
		}
	}()

	ts.Tables, err = ts.TableRepo.GetByDBName(dbName)

	return err

}

// getStatisticsByHostInfoAndDBNameAndTableNameWithHTTP gets the table statistics by host info and db name and table name with http
func (ts *TableService) getStatisticsByHostInfoAndDBNameAndTableNameWithHTTP(hostIP string, portNum int, dbName, tableName, loginName string) error {
	if viper.GetString(config.RoleKey) == config.ServerRole {
		// get mysql server
		mysqlService := NewMySQLServerServiceWithDefault()
		err := mysqlService.GetByHostInfo(hostIP, portNum)
		if err != nil {
			return err
		}
		// check accessibility
		deploymentService := deployment.NewServiceWithDefault(mysqlService.GetMySQLServers()[constant.ZeroInt])
		if !deploymentService.CheckAccessibility() {
			// prepare data
			data := []byte(fmt.Sprintf(utilmeta.TableHostInfoAndDBNameAndTableNameHTTPBodyTemplate, hostIP, portNum, dbName, tableName, loginName))
			// send request
			respBody, err := deploymentService.PostDAS(utilmeta.TableGetStatisticsByHostInfoAndDBNameAndTableNamePath, data)
			if err != nil {
				return err
			}
			// parse response body
			return ts.parseGetStatisticsResponseBody(respBody)
		}
	}

	return ts.GetStatisticsByHostInfoAndDBNameAndTableNameLocal(hostIP, portNum, dbName, tableName)
}

// GetStatisticsByHostInfoAndDBNameAndTableNameLocal gets the table statistics by host info and db name and table name
func (ts *TableService) GetStatisticsByHostInfoAndDBNameAndTableNameLocal(hostIP string, portNum int, dbName, tableName string) error {
	// mysql server is available or the role is agent
	err := ts.TableRepo.InitMySQLConn(hostIP, portNum, dbName)
	if err != nil {
		return err
	}
	defer func() {
		closeErr := ts.TableRepo.Close()
		if closeErr != nil {
			log.Errorf("metadata TableService.GetStatisticsByHostInfoAndDBNameAndTableNameLocal(): close mysql connection failed. error:\n%+v", closeErr)
		}
	}()

	ts.TableStatistics, ts.IndexStatistics, ts.CreateStatement, err = ts.TableRepo.GetStatisticsByDBNameAndTableName(dbName, tableName)

	return err

}

// parseGetStatisticsResponseBody parses the response body of statistics
func (ts *TableService) parseGetStatisticsResponseBody(respBody []byte) error {
	t := &struct {
		TableStatistics []*TableStatistic `json:"table_statistics"`
		IndexStatistics []*IndexStatistic `json:"index_statistics"`
		CreateStatement string            `json:"create_statement"`
	}{}

	// unmarshal
	err := json.Unmarshal(respBody, &t)
	if err != nil {
		return err
	}
	// table statistics
	ts.TableStatistics = nil
	for _, tableStatistic := range t.TableStatistics {
		ts.TableStatistics = append(ts.TableStatistics, tableStatistic)
	}
	// index statistics
	ts.IndexStatistics = nil
	for _, indexStatistic := range t.IndexStatistics {
		ts.IndexStatistics = append(ts.IndexStatistics, indexStatistic)
	}
	// create statement
	ts.CreateStatement = t.CreateStatement

	return nil
}

// analyzeTableByHostInfoAndDBNameAndTableNameWithHTTP analyzes the table by host info and db name and table name with http
func (ts *TableService) analyzeTableByHostInfoAndDBNameAndTableNameWithHTTP(hostIP string, portNum int, dbName, tableName, loginName string) error {
	if viper.GetString(config.RoleKey) == config.ServerRole {
		// get mysql server
		mysqlService := NewMySQLServerServiceWithDefault()
		err := mysqlService.GetByHostInfo(hostIP, portNum)
		if err != nil {
			return err
		}
		// check accessibility
		deploymentService := deployment.NewServiceWithDefault(mysqlService.GetMySQLServers()[constant.ZeroInt])
		if !deploymentService.CheckAccessibility() {
			// prepare data
			data := []byte(fmt.Sprintf(utilmeta.TableHostInfoAndDBNameAndTableNameHTTPBodyTemplate, hostIP, portNum, dbName, tableName, loginName))
			// send request
			_, err = deploymentService.PostDAS(utilmeta.TableAnalyzeTableByHostInfoAndDBNameAndTableNamePath, data)

			return err
		}
	}

	return ts.AnalyzeTableByHostInfoAndDBNameAndTableNameLocal(hostIP, portNum, dbName, tableName)
}

// AnalyzeTableByHostInfoAndDBNameAndTableNameLocal analyzes the table by host info and db name and table name
func (ts *TableService) AnalyzeTableByHostInfoAndDBNameAndTableNameLocal(hostIP string, portNum int, dbName, tableName string) error {
	// mysql server is available or the role is agent
	err := ts.TableRepo.InitMySQLConn(hostIP, portNum, dbName)
	if err != nil {
		return err
	}
	defer func() {
		closeErr := ts.TableRepo.Close()
		if closeErr != nil {
			log.Errorf("metadata TableService.AnalyzeTableByHostInfoAndDBNameAndTableNameLocal(): close mysql connection failed. error:\n%+v", closeErr)
		}
	}()

	return ts.TableRepo.AnalyzeTableByDBNameAndTableName(dbName, tableName)
}
